Java Runtime Types topic
Runtime type checks in Java (JNI)
Ordinary Dart has a distinction between an object's static type and its runtime type:
class Base {}
class Child extends Base {}
Base x = Child(); // x has a static type of Base
print(x.runtimeType); // but a runtime type of Child
The static type determines at compile time what methods are allowed to be invoked on an object, and the runtime type determines which class's method implementation is actually invoked at runtime.
When doing Java interop via JNI, another layer of typing is added to each
variable. As well as the Dart static type and runtime type, there is also the
Java runtime type to consider. The (Dart static/Dart runtime/Java runtime)
types could come in just about any combination, e.g. (Base/Child/Child),
(Base/Base/Child), or even (Base/Child/Grandchild).
Just like in the pure Dart case, the Dart static type determines
what methods are allowed to be invoked, but now the method
implementation that is actually invoked at run time is determined
by the Java runtime type. In fact, the Dart runtime type is
completely irrelevant when doing Java interop. Moreover, the
Dart wrappers around Java objects are extension types,
which means their Dart runtime type (aka representation type) will always be
JObject.
Dart's is keyword checks the Dart runtime type. You shouldn't use
this on Java objects, because the Dart runtime type is always
JObject. If Foo and Bar are unrelated Java classes wrapped as
extension types, and x is a Foo, then x is Bar will be true at runtime,
making these checks useless and misleading. Instead of x is Foo, use
x.isA(Foo.type), which calls into JNI to check the runtime type of the
underlying Java object.
Dart's as keyword changes the static type of an object (and also
checks its runtime type). Since the Java wrapper objects are
extension types, this works, but is unsafe. The implicit is check
that as performs is useless, for the reasons mentioned above.
Instead of x as Foo, use x.as(Foo.type), which internally checks
x.isA(Foo.type).
Generic type erasure
Because of Java generic type erasure, isA cannot distinguish between
generic variations of the same class. For example, Foo<A> and Foo<B>
are both of type Foo at runtime in Java. Therefore, even if JNIgen had syntax
like Foo.type(A.type) (which it doesn't), object.isA(Foo.type(A.type))
would return true for objects of type Foo<B> as well. So JNIgen simply
generates Foo.type even for generic classes.
Memory management when casting
When casting using .as(), you can optionally release the original reference
by passing releaseOriginal: true. This is useful to keep the number of active
global JNI references low.
final JNumber num = foo.someJNumber();
// Cast to JInteger and release the original JNumber reference.
final JInteger jint = num.as(JInteger.type, releaseOriginal: true);
// It's not safe to use num after this.
Classes
- JniGenerator Java Differences Lifecycle Threading Interface Implementation Exceptions Java Runtime Types Debugging
- Configuration for JNIgen binding generation.